package jmine.tec.persist.schema.impl.elements.type;

import java.sql.Types;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import jmine.tec.persist.schema.impl.elements.ColumnTypeElement;
import jmine.tec.persist.schema.impl.elements.Schema;

import org.hibernate.type.Type;

/**
 * Estratégia base para enums java
 * 
 * @author lundberg
 */
public abstract class AbstractEnumTypeStrategy implements TypeStrategy {

    /**
     * {@inheritDoc}
     */
    public Set<AllowedValue> getAllowedValues(Class<?> returnedClass) {
        return this.doGetAllowedValues(returnedClass.asSubclass(Enum.class));
    }

    /**
     * Obtém os valores permitidos para a enum
     * 
     * @param <E> tipo da enum
     * @param returnedClass classe da enum
     * @return conjunto de valores permitidos
     */
    private <E extends Enum<?>> Set<AllowedValue> doGetAllowedValues(Class<E> returnedClass) {
        Set<AllowedValue> values = new HashSet<AllowedValue>();
        for (E e : returnedClass.getEnumConstants()) {
            values.add(new IntegerAllowedValue(this.getElementValue(e), e.name().toUpperCase()));
        }
        return values;
    }

    /**
     * Encontra a precisão mínima necessária pela coluna para suportar os elementos da enum
     * 
     * @param <E> tipo de enum
     * @param enumClass enumClass
     * @return int
     */
    protected <E extends Enum<?>> int minimumPrecision(Class<E> enumClass) {
        int max = 0;
        for (E e : enumClass.getEnumConstants()) {
            max = Math.max(max, this.getElementValue(e));
        }
        return Integer.toString(max).length();
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasLimitedAllowedValues() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void inferTypeParameters(ColumnTypeElement columnType, Class<?> returnedClass) {
        columnType.setPrecision(this.minimumPrecision(returnedClass.asSubclass(Enum.class)));
        columnType.setScale(0);
    }

    /**
     * {@inheritDoc}
     */
    public boolean matches(ColumnTypeElement columnTypeElement, Type type) {
        return type.getReturnedClass().isEnum();
    }

    /**
     * {@inheritDoc}
     */
    public List<String> checkUserDefinedValues(ColumnTypeElement columnType, Class<?> returnedClass) {
        List<String> warnings = new LinkedList<String>();
        if (columnType.getScale() != 0) {
            warnings.add(String.format("Scale definida para coluna %s, usada por Enum", columnType.getColumn().toString()));
        }
        if (columnType.getPrecision() - columnType.getScale() < this.minimumPrecision(returnedClass.asSubclass(Enum.class))) {
            warnings.add(String.format("Precision definido abaixo do necessário para coluna %s, usada por Enum.", columnType.getColumn()
                    .toString()));
            columnType.setPrecision(columnType.getScale() + this.minimumPrecision(returnedClass.asSubclass(Enum.class)));
        }
        return warnings;
    }

    /**
     * {@inheritDoc}
     */
    public int correctSqlType(Type type, int current) {
        if (current != Types.VARBINARY && current != Types.NUMERIC) {
            Schema.LOG.warn(String.format("Coluna %s mapeada para tipos sql distintos: %s e %s", current, Types.VARBINARY, Types.NUMERIC));
        }
        return Types.NUMERIC;
    }

    /**
     * Obtém o valor utilizado para representar o elemento da enumeração no banco de dados.
     * 
     * @param <E> tipo de elemento
     * @param element element
     * @return valor
     */
    protected abstract <E extends Enum<?>> int getElementValue(E element);
}